package pl.edu.mimuw.mas.util;

import jade.core.AID;
import jade.domain.FIPANames;
import jade.lang.acl.ACLMessage;

import java.util.HashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import pl.edu.mimuw.mas.agent.AbstractAgent;
import pl.edu.mimuw.mas.agent.DeerAgent;
import pl.edu.mimuw.mas.agent.HelicopterAgent;
import pl.edu.mimuw.mas.chart.Area;
import pl.edu.mimuw.mas.chart.Chart;
import pl.edu.mimuw.mas.ontology.VisualizationCoreOntology;

public final class PositionHelper {

	protected static final Logger logger = LoggerFactory.getLogger(PositionHelper.class);

	@SuppressWarnings("unchecked")
	private static HashMap<Class, HashMap<AID, Position>> positions = new HashMap<Class, HashMap<AID, Position>>();
	private static Position deerPosition = null;
	
	public static void move(AbstractAgent a, Move move) {
		int vx = move.vx;
		int vy = move.vy;
		
		// ruszam sie
		Chart chart = a.getChart();
		Position p = a.getPosition();
		p.x = Math.max(Math.min(chart.getColumnsCount() - 1, p.x + vx), 0);
		p.y = Math.max(Math.min(chart.getColumnsCount() - 1, p.y + vy), 0);
		p.z = chart.getHeight(p.x, p.y);

		// zapamietuje nowa pozycje
		if (!positions.containsKey(a.getClass())) {
			positions.put(a.getClass(), new HashMap<AID, Position>());
		}
		positions.get(a.getClass()).put(a.getAID(), p);
		
		// specjalny wariant dla łosia
		if(a instanceof DeerAgent) {
			deerPosition = p;
		}

		// wysylam informacje do wizualizatora
		try {
			ACLMessage msg = new ACLMessage(ACLMessage.REQUEST);

			if (a instanceof DeerAgent) {
				msg.addReceiver(a.getViz2DAgent());
			}
			msg.addReceiver(a.getViz3DAgent());
			msg.setOntology(VisualizationCoreOntology.ONTOLOGY_NAME);
			msg.setLanguage(FIPANames.ContentLanguage.FIPA_SL);
			
			// brzydko, ale sa dwie klasy o tej samej nazwie
			pl.edu.mimuw.mas.ontology.concept.Position pos;
			pos = new pl.edu.mimuw.mas.ontology.concept.Position(a.getName(),
					p.x, p.y, p.z, a.getR(), a.getG(), a.getB());
			a.getContentManager().fillContent(msg, pos);

			a.send(msg);
		} catch (Exception e) {
			logger.error("{}: Błąd podczas przesuwania", a.getName(), e);
		}
	}
	
	public static void fly(AbstractAgent a, Move move) {
		int vx = move.vx;
		int vy = move.vy;
		
		// ruszam sie
		Chart chart = a.getChart();
		Position p = a.getPosition();
		p.x = Math.max(Math.min(chart.getColumnsCount() - 1, p.x + vx), 0);
		p.y = Math.max(Math.min(chart.getColumnsCount() - 1, p.y + vy), 0);

		// zapamietuje nowa pozycje
		if (!positions.containsKey(a.getClass())) {
			positions.put(a.getClass(), new HashMap<AID, Position>());
		}
		positions.get(a.getClass()).put(a.getAID(), p);

		// wysylam informacje do wizualizatora
		try {
			ACLMessage msg = new ACLMessage(ACLMessage.REQUEST);

			if (a instanceof DeerAgent) {
				msg.addReceiver(a.getViz2DAgent());
			}
			msg.addReceiver(a.getViz2DAgent());
			msg.setOntology(VisualizationCoreOntology.ONTOLOGY_NAME);
			msg.setLanguage(FIPANames.ContentLanguage.FIPA_SL);
			
			pl.edu.mimuw.mas.ontology.concept.Position pos;
			pos = new pl.edu.mimuw.mas.ontology.concept.Position(a.getName(), p.x, p.y, a.getR(), a.getG(), a.getB());
			a.getContentManager().fillContent(msg, pos);

			a.send(msg);
		} catch (Exception e) {
			logger.error("{}: Błąd podczas przesuwania", a.getName(), e);
		}
	}

	public static void flyRefresh(AbstractAgent a) {
		Position p = a.getPosition();
		
		try {
			ACLMessage msg = new ACLMessage(ACLMessage.REQUEST);
			if (a instanceof DeerAgent) {
				msg.addReceiver(a.getViz2DAgent());
			}
			msg.addReceiver(a.getViz2DAgent());
			msg.setOntology(VisualizationCoreOntology.ONTOLOGY_NAME);
			msg.setLanguage(FIPANames.ContentLanguage.FIPA_SL);
			
			pl.edu.mimuw.mas.ontology.concept.Position pos;
			pos = new pl.edu.mimuw.mas.ontology.concept.Position(a.getName(), p.x, p.y, a.getR(), a.getG(), a.getB());
			a.getContentManager().fillContent(msg, pos);

			a.send(msg);
		} catch (Exception e) {
			logger.error("{}: Błąd podczas przesuwania", a.getName(), e);
		}
	}
	
	/**
	 * Zwraca średnią odległość do agentów tej samej klasy
	 */
	@SuppressWarnings("unchecked")
	public static float getAverageDistance(AbstractAgent a, Position p) {
		Class clazz = a.getClass();
		float dist = 0;
		float d;
		int counter = 0;
		
		for (Class cls : positions.keySet()) {
			if (clazz == cls || clazz == null) {
				for (AID aid : positions.get(clazz).keySet()) {
					if (aid != a.getAID()) {
						d = getDistance(p, positions.get(clazz).get(aid));
						if (d < 50f) {
							counter += 1;
							dist += d;
						}
					}
				}
			}
		}
		if (counter != 0) {
			dist /= counter;
		}
		
		return dist;
	}

	public static float getDistance(Position p1, Position p2) {
		Double ret = Math.sqrt(power(p1.x - p2.x) + power(p1.y - p2.y));
		return ret.floatValue();
	}

	public static float getDistance(int x0, int y0, int x1, int y1) {
		Double ret = Math.sqrt(power(x1 - x0) + power(y1 - y0));
		return ret.floatValue();
	}
	
	private static int power(int n) {
		return n * n;
	}
	
	public static Position getRandomPositionInAir(AbstractAgent a) {
		Position pos = new Position();
		pos.x = (int) (Math.random() * (a.getChart().getColumnsCount() - 1));
		pos.y = (int) (Math.random() * (a.getChart().getRowsCount() - 1));
		
		return pos;
	}
	
	public static Position getRandomPosition(AbstractAgent a) {
		Position pos = new Position();
		pos.x = (int) (Math.random() * (a.getChart().getColumnsCount() - 1));
		pos.y = (int) (Math.random() * (a.getChart().getRowsCount() - 1));
		pos.z = a.getChart().getHeight(pos.x, pos.y); 

		return pos;
	}
	
	public static MoveTrend getNextSearchPosition(Area area, Position cur, Position dest, MoveTrend trend) {
		if(!area.isInArea(cur.x, cur.y)) {			
			getInitialSearchPosition(area, dest);
			return MoveTrend.RIGHT;
		}
		
		int[] realArea = area.getArea();
		
		if(trend == MoveTrend.RIGHT) {
			dest.x = Math.min(cur.x + 2 * Range.HELICOPTER_SEARCH_RANGE, realArea[Area.X1] - Range.HELICOPTER_SEARCH_RANGE);
			
			if(dest.x == realArea[Area.X1] - Range.HELICOPTER_SEARCH_RANGE) {
				trend = MoveTrend.DOWN;
			}			
		} else if(trend == MoveTrend.DOWN) {
			dest.y = Math.max(cur.y - 2 * Range.HELICOPTER_SEARCH_RANGE, realArea[Area.Y0] + Range.HELICOPTER_SEARCH_RANGE);
			
			if(dest.x == realArea[Area.X1] - Range.HELICOPTER_SEARCH_RANGE) {
				trend = MoveTrend.LEFT;	
			} else {
				trend = MoveTrend.RIGHT;
			}
		} else if(trend == MoveTrend.LEFT) {
			if(dest.y == realArea[Area.Y0] + Range.HELICOPTER_SEARCH_RANGE) {
				dest.x = Math.max(cur.x - 2 * Range.HELICOPTER_SEARCH_RANGE, realArea[Area.X0] + Range.HELICOPTER_SEARCH_RANGE);
				
				if(dest.x == realArea[Area.X0] + Range.HELICOPTER_SEARCH_RANGE) {
					trend = MoveTrend.UP;	
				}				
			} else {
				dest.x = Math.max(cur.x - 2 * Range.HELICOPTER_SEARCH_RANGE, realArea[Area.X0] + 3 * Range.HELICOPTER_SEARCH_RANGE);
				
				if(dest.x == realArea[Area.X0] + 3 * Range.HELICOPTER_SEARCH_RANGE) {
					trend = MoveTrend.DOWN;	
				}
			}			
		} else if(trend == MoveTrend.UP) {
			dest.y = Math.min(cur.y + 2 * Range.HELICOPTER_SEARCH_RANGE, realArea[Area.Y1] - Range.HELICOPTER_SEARCH_RANGE);
			if(dest.y == realArea[Area.Y1] - Range.HELICOPTER_SEARCH_RANGE) {
				trend = MoveTrend.RIGHT;	
			}
		}
		
		return trend;
	}
	
	public static void getInitialSearchPosition(Area area, Position dest) {
		int[] realArea = area.getArea();
		dest.x = realArea[Area.X0] + Range.HELICOPTER_SEARCH_RANGE; 
		dest.y = realArea[Area.Y1] - Range.HELICOPTER_SEARCH_RANGE;
	}
	
	/**
	 * 100% pewne sprawdzenie czy w zasiegu helikoptera na pozycji
	 * <code>pos</code> niema łosia.
	 */
	public static boolean verifyDeer(Position pos) {
		for(Move mv : Range.HELICOPTER_SEARCH_CAPABILITY) {
			if(deerPosition.equals(pos, mv)) {
				return true;
			}
		}
		
		return false;
	}

	/**
	 * Szukanie łosia przez helikopter z pozycji <code>pos</code>. Jesli jest
	 * łoś to zostanie zauważony jeśli nie to helikopter może się pomylić i
	 * zobczyć łosia którego niema w losowym miejscu w zasięgu, w przeciwnym
	 * przypadku zwraca <code>null</code>.
	 */
	public static Position searchDeerInRange(HelicopterAgent ha) {
		for(Move mv : Range.HELICOPTER_SEARCH_CAPABILITY) {
			if(deerPosition.equals(ha.getPosition(), mv)) {
				return deerPosition;
			}
		}
		
		if(Math.random() < Probability.HELICOPTER_SEARCH_MISTAKE) {
			return getRandomPositionInRange(ha);
		}
		
		return null;
	}
	
	/**
	 * Szukanie łosia w przypadku sledzenia, jesli los sie pomylil to myli sie
	 * nadal lata glupek w losowe miejsca na w obszarze widoczności. Jeśli się
	 * nie pomylił to lata za łosiem.
	 */
	public static Position followDeerInRange(HelicopterAgent ha) {
		for(Move mv : Range.HELICOPTER_SEARCH_CAPABILITY) {
			if(deerPosition.equals(ha.getPosition(), mv)) {
				return deerPosition;
			}
		}
		
		return getRandomPositionInRange(ha);
	}
	
	/**
	 * Zwraca losową pozycję w zasięgu widoczności helikoptera.
	 */
	private static Position getRandomPositionInRange(HelicopterAgent ha) {
		int index = (int) (Math.random() * (Range.HELICOPTER_SEARCH_CAPABILITY.size() - 1));
				
		Position pos = ha.getPosition();		
		Position res = new Position();

		Move mv = Range.HELICOPTER_SEARCH_CAPABILITY.get(index);
		
		int xmax = ha.getChart().getColumnsCount();
		int ymax = ha.getChart().getRowsCount();		
		
		res.x = Math.min(Math.max(pos.x + mv.vx, 0), xmax);
		res.y = Math.min(Math.max(pos.y + mv.vy, 0), ymax);
		
		return res;
	}
	
	/**
	 * Zwraca pozycję łosia.
	 * @return pozycja łosia
	 */
	public static Position getDeerPosition() {
		return deerPosition;
	}
	
	/**
	 * Sprawdza czy łoś jest widoczny przez agenta naziemnego z pozycji <code>pos</code>
	 * @param pos pozycja agenta
	 * @return prawda jeśli łoś jest widoczny
	 */
	public static boolean isDeerVisible(Position pos) {
		if (deerPosition == null) {
			return false;
		}
		else if (getDistance(pos, deerPosition) < Range.DEER_VISIBILITY) {
			return true;
		} else {
			return false;
		}
	}
	
	public static Move getRandomMove() {
		int vx = (int) Math.round(Math.random() * 2.0 - 1.0);
		int vy = (int) Math.round(Math.random() * 2.0 - 1.0);
		return new Move(vx, vy);
	}
	 
	@SuppressWarnings("unchecked")
	public static Position getPosition(Class c, AID aid) {
		return positions.get(c).get(aid);
	}
}
